package com.klikmee.sms.core.base.communicator.mts;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.text.MessageFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.activation.DataHandler;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.handler.MessageContext;

import org.apache.cxf.endpoint.Client;
import org.apache.cxf.endpoint.Endpoint;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor;
import org.apache.log4j.Logger;
import org.apache.ws.security.WSConstants;
import org.apache.ws.security.WSPasswordCallback;
import org.apache.ws.security.handler.WSHandlerConstants;

import com.klikmee.sms.core.base.Core;
import com.klikmee.sms.core.base.communicator.Communicator;
import com.klikmee.sms.core.base.communicator.DeliveryReport;
import com.klikmee.sms.core.base.communicator.SendSmsIntercepter;
import com.klikmee.sms.core.base.dao.ProviderDAO;
import com.klikmee.sms.core.base.subscription.NoDistributionListFoundException;
import com.klikmee.sms.core.base.subscription.SubscriptionManager;
import com.klikmee.sms.core.base.util.Constants;
import com.klikmee.sms.core.base.util.DeliveryReports;
import com.klikmee.sms.core.model.BulkMessage;
import com.klikmee.sms.core.model.IncomingMessage;
import com.klikmee.sms.core.model.OutgoingMessage;
import com.klikmee.sms.core.model.Provider;
import com.klikmee.sms.core.ws.mts.client.receivemsg.ReceiveMessage;
import com.klikmee.sms.core.ws.mts.client.receivemsg.ReceiveMessageService;
import com.klikmee.sms.core.ws.mts.client.sendsms.ChargingInformation;
import com.klikmee.sms.core.ws.mts.client.sendsms.PolicyException_Exception;
import com.klikmee.sms.core.ws.mts.client.sendsms.SendSms;
import com.klikmee.sms.core.ws.mts.client.sendsms.SendSmsService;
import com.klikmee.sms.core.ws.mts.client.sendsms.ServiceException_Exception;
import com.klikmee.sms.core.ws.mts.client.sendsms.SimpleReference;
import com.klikmee.sms.core.ws.mts.listener.MTSCommunicator;
import com.klikmee.sms.core.ws.mts.listener.msgnotification.MessagePriority;
import com.klikmee.sms.core.ws.mts.listener.smsnotification.DeliveryInformation;
import com.klikmee.sms.core.ws.mts.listener.smsnotification.DeliveryStatus;
import com.klikmee.sms.core.ws.mts.listener.smsnotification.SmsMessage;
import com.klikmee.sms.core.ws.mts.listener.subscription.SubscriptionDetail;

public class MTSCommunicatorImpl implements Communicator, MTSCommunicator {

	// logger instance
	private Logger						logger							= Logger.getLogger( MTSCommunicatorImpl.class );

	// core instance used to further process messages after they are recevied
	// from MTS carrier
	private Core						core							= null;

	// wsdl urls
	private URL							sendSMSURL						= null;
	private URL							receiveMessageURL				= null;
	// send SMS service proxy
	private SendSms						sendsmsProxy					= null;
	private ReceiveMessage				receiveMessageProxy				= null;
	private String						deliveryReportNotificationURL	= null;
	private String						sendSmsOverrideAddress			= null;
	// ref to mts provider
	private Provider					mtsProvider;
	private SubscriptionManager			subscriptionManager				= null;
	private ProviderDAO					providerDAO						= null;

	private String						providerId						= null;
	private String						providerPassword				= null;

	private List<SendSmsIntercepter>	sendSmsIntercepters				= null;

	// init method
	public void init() {

		// init ws proxies
		this.sendsmsProxy = new SendSmsService( sendSMSURL ).getSendSms();
		this.receiveMessageProxy = new ReceiveMessageService( this.receiveMessageURL ).getReceiveMessage();

		// override endpoing address
		overrideWsEndpoints();
		// load mts provider
		this.mtsProvider = this.providerDAO.findByName( Constants.PROVIDER_MTS );

		logger.info( "MTS communicator successfuly initialized." );
	}

	protected void overrideWsEndpoints() {
		if (this.sendSmsOverrideAddress != null) {
			BindingProvider sendsmsBP = (BindingProvider) this.sendsmsProxy;
			Map<String, Object> sendsmsRequestContext = sendsmsBP.getRequestContext();
			sendsmsRequestContext.put( BindingProvider.ENDPOINT_ADDRESS_PROPERTY, this.sendSmsOverrideAddress );

			logger.info( String.format( "Sendsms service url [%s]", this.sendSmsOverrideAddress ) );
		}
	}

	/**
	 * utility method to set soap header authentication. MTS uses username token
	 * security standard by OASIS.
	 * 
	 * @param username
	 */
	private void setHeaders(String username) {
		// reset send sms proxy
		this.sendsmsProxy = new SendSmsService( sendSMSURL ).getSendSms();
		overrideWsEndpoints();
		
		Client client = ClientProxy.getClient( sendsmsProxy );
		Endpoint cxfEndpoint = client.getEndpoint();

		Map<String, Object> outProps = new HashMap<String, Object>();
		outProps.put( WSHandlerConstants.ACTION, WSHandlerConstants.USERNAME_TOKEN );
		outProps.put( WSHandlerConstants.USER, username );
		outProps.put( WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_DIGEST );

		outProps.put( WSHandlerConstants.PW_CALLBACK_CLASS, ClientPasswordCallback.class.getName() );
		// put additional stuff in header
		outProps.put( WSHandlerConstants.ADD_UT_ELEMENTS, WSConstants.NONCE_LN + " " + WSConstants.CREATED_LN );

		WSS4JOutInterceptor interceptor = new WSS4JOutInterceptor( outProps );
		cxfEndpoint.getOutInterceptors().add( interceptor );

	}

	public void sendMessage(OutgoingMessage outgoingMessage) {

		// set request parameters

		// set recipient of message
		List<String> addresses = new LinkedList<String>();
		// check if outgoing message has session id
		if (outgoingMessage.getMtsSessionId() != null) {
			addresses.add( outgoingMessage.getMtsSessionId() );
		}
		// otherwise use phone number
		else {
			addresses.add( "tel:" + outgoingMessage.getToNumber() );
		}

		// sender name
		// String senderName =
		// outgoingMessage.getPaymentInvoiceList().iterator().next().getServiceChargingCategory().getService().getMtsServiceName();
		String senderName = outgoingMessage.getFromNumber();

		// charging info
		ChargingInformation charging = new ChargingInformation();
		charging.setCode( "" );
		charging.setDescription( "" );
		charging.setAmount( null );
		charging.setCurrency( "" );
		// TODO: check are the other field in this object necessary for regular
		// sms

		// message text
		String message = outgoingMessage.getMessage();

		// status report delivery info
		SimpleReference receiptRequest = new SimpleReference();
		// correlator used to link delivery report with this message. Core
		// message id is used for this
		receiptRequest.setCorrelator( String.valueOf( outgoingMessage.getId() ) );
		receiptRequest.setEndpoint( this.deliveryReportNotificationURL );
		// this parameter is ignored
		receiptRequest.setInterfaceName( "not used" );

		// set headers
		// username is composed of <serviceSendingId>@<providerid>
		String username = outgoingMessage.getPaymentInvoiceList().iterator().next().getServiceChargingCategory().getMtsSendingId() + "@" + this.providerId;
		this.setHeaders( username );

		try {
			logger.debug( MessageFormat.format( "Trying to send sms message with params addr[{0}], sender[{1}], username[{2}], message[{3}]", addresses, senderName, username, message ) );
			String requestIdentifier = sendsmsProxy.sendSms( addresses, senderName, null, message, receiptRequest );
			outgoingMessage.setProvidersMesssageId( requestIdentifier );
			logger.info( MessageFormat.format( "Successfully sent message with id = [{0}]", outgoingMessage.getId() ) );
			// if everything is ok, notify core
			core.onMessageSent( outgoingMessage );

		}
		catch (PolicyException_Exception e) {
			logger.fatal( String.format( "Policy exception while trying to send message id = %s", outgoingMessage.getId() ), e );
		}
		catch (ServiceException_Exception e) {

			logger.fatal( String.format( "Service exception while trying to send message id = %s", outgoingMessage.getId() ), e );
		}
	}

	public void onDeliveryReceipt(String correlator, DeliveryInformation deliveryInformaion) {
		// this object will be forwarded to core
		DeliveryReport deliveryReport = new DeliveryReport();

		// set provides messages id
		deliveryReport.setMessageId( Long.parseLong( correlator ) );
		// resolve status report
		int resolvedStatus = resolveDeliveryStatus( deliveryInformaion.getDeliveryStatus() );
		deliveryReport.setStatus( resolvedStatus );

		deliveryReport.setProvider( this.mtsProvider );

		// try to deliver message to core
		try {
			core.enqueueStatusReport( deliveryReport );
			logger.info( MessageFormat.format( "Received status report: corellator[{0}], mtsStatus[{1}], coreStatus[{2}]", correlator, deliveryInformaion.getDeliveryStatus(), resolvedStatus ) );
		}
		catch (Exception e) {
			logger.fatal( String.format( "Failed to forward delivery status to core, correlator = [%s] ", correlator ) );
		}
	}

	/**
	 * This method is invoked when new sms message is received. Message
	 * parameters are extracted and Incoming message is created and forwarded to
	 * core.
	 */
	public void onSmsReception(String correlator, SmsMessage mtsMessage) {
		// this object will be forwarded to core
		IncomingMessage incomingMessage = new IncomingMessage();
		incomingMessage.setTypeOfMessage( Constants.MESSAGE_TYPE_SMS );
		incomingMessage.setOurIncomingTime( new Date() );
		incomingMessage.setProvider( this.mtsProvider );

		incomingMessage.setProviderMessageId( correlator );
		incomingMessage.setToNumber( mtsMessage.getSmsServiceActivationNumber().substring( "tel:".length() ) );

		// extract phone number and session id

		String[] sessionParameters = this.extractPhonenumberFromSessionId( mtsMessage.getSenderAddress() );
		if (sessionParameters != null) {
			incomingMessage.setMtsSessionId( sessionParameters[0] );
			incomingMessage.setFromNumber( sessionParameters[1] );
		}
		else {
			// use format tel:38164100100
			incomingMessage.setFromNumber( mtsMessage.getSenderAddress().substring( "tel:".length() ) );
		}

		incomingMessage.setMessage( mtsMessage.getMessage() );
		logger.info( MessageFormat.format( "Received message: referenceId[{0}], from[{1}]", correlator, incomingMessage.getFromNumber()) );
		// try to delive message to core
		try {
			core.enqueueIncomingMessage( incomingMessage );
		}
		catch (Exception e) {
			logger.fatal( String.format( "Failed to deliver message to core, reference id = [%s]", correlator ), e );
		}

	}

	@Override
	public boolean canHandleBulkMessage(String receiver) {
		// TODO Auto-generated method stub
		return false;
	}

	public String[] extractPhonenumberFromSessionId(String sessionId) {
		// first element of array is sessionId and the second is sender's phone
		// number
		String[] incommingParameters = null;

		if (sessionId == null) {
			return null;
		}

		int indexOfSeparator = sessionId.indexOf( '#' );

		// check if sessionId contains #
		if (indexOfSeparator != -1) {
			incommingParameters = new String[2];
			// extract values
			incommingParameters[0] = sessionId.substring( 0, indexOfSeparator );
			incommingParameters[1] = sessionId.substring( indexOfSeparator + 1 );
		}

		return incommingParameters;
	}

	public void sendBulkMessage(BulkMessage bulkMessage) {
		List<String> addresses = new LinkedList<String>();
		// add necessary prefixes
		// check if recipient is single user
		String recipient = "";
		if (bulkMessage.getToNumber().startsWith( "381" )) {
			recipient = "tel:" + bulkMessage.getToNumber();
		}
		// if not then it's a name of download list
		else {
			recipient = "dl:" + bulkMessage.getToNumber();
		}
		addresses.add( recipient );

		// set sender name
		String senderName = bulkMessage.getSenderId();

		// charging should stay empty
		ChargingInformation charging = new ChargingInformation();

		// message
		String message = bulkMessage.getText();

		// this sould be null because it's not possible to get delivery for bulk
		SimpleReference receiptRequest = null;
		try {
			String username = bulkMessage.getChargingCategory().getMtsSendingId() + "@" + this.providerId;
			setHeaders( username );
			String sendBulkResult = sendsmsProxy.sendSms( addresses, senderName, charging, message, receiptRequest );
			core.onBulkMessageSent( bulkMessage );

		}
		catch (PolicyException_Exception e) {
			logger.fatal( String.format( "Policy exception while trying to send bulk message id = %s", bulkMessage.getId() ) );
		}
		catch (ServiceException_Exception e) {
			logger.fatal( String.format( "Service exception while trying to send bulk message id = %s", bulkMessage.getId() ) );
		}

	}

	public SubscriptionDetail getSubscriptionDetail(String serviceId, String message) throws com.klikmee.sms.core.ws.mts.listener.subscription.ServiceException_Exception {

		// try to find distribution list data
		try {

			Hashtable<String, String> paramsTable = subscriptionManager.getSubscriptionDetail( serviceId, message );

			SubscriptionDetail subscriptionDetail = new SubscriptionDetail();
			subscriptionDetail.setDistributionList( paramsTable.get( SubscriptionManager.DISTRIBUTION_LIST_ID ) );
			subscriptionDetail.setExtensionParameter( paramsTable.get( SubscriptionManager.DISTRIBUTION_LIST_NAME ) );

			return subscriptionDetail;

		}
		catch (NoDistributionListFoundException e) {
			// it this exception is cought then no distribution list is found
			// for query, throw service ex
			throw new com.klikmee.sms.core.ws.mts.listener.subscription.ServiceException_Exception();
		}

		// in case of any other exception
		catch (Exception e) {
			throw new com.klikmee.sms.core.ws.mts.listener.subscription.ServiceException_Exception( "Unexpected error", e );
		}

	}

	public void receiveSubscription(String msisdn, String serviceId, String distributionList, Date validTo) {
		// forward request to subaciption mngr
		this.subscriptionManager.receiveSubacription( msisdn, serviceId, distributionList, validTo );

	}

	public void receiveUnsubscription(String msisdn, String serviceId, String distributionListId) {
		// forward request to subscription mngr
		this.subscriptionManager.receiveUnsubscription( msisdn, serviceId, distributionListId );

	}

	public void triggerService(String serviceId, String distributionList) {
		// TODO Auto-generated method stub

	}

	public void receiveSubscriptionExtension(String msisdn, String serviceId, String distributionList, Date validTo) {
		// forward request to subscription mngr
		this.subscriptionManager.receiveSubscriptionExtension( msisdn, serviceId, distributionList, validTo );
	}

	/**
	 * Invoked when MMS message arrives from MTS. if message identifier
	 * parameter is present, it means that there is an actual message waiting on
	 * MTS gateway, so we pick it up with a call to getMessage(identifier)
	 * method of receiveMessage Service. If not, then it's a MMS message with
	 * only plain text and that text is contained in message parameter.
	 * 
	 */

	public void notifyMessageReception(String correlator, String messageIdentifier, String message, Date time, String messageServiceActivationNumber, String senderAddress, String subject,
			MessagePriority priority) {

		// set up incoming msg

		IncomingMessage incomingMessage = new IncomingMessage();
		incomingMessage.setProvider( mtsProvider );

		// check if message contains messageIdentifier
		if (messageIdentifier == null || messageIdentifier.isEmpty()) {
			// get message attachments

		}

	}

	/**
	 * This method gets attachments for MMS message using getMessage web service
	 * proxy.
	 * 
	 * @param messageIdentifier
	 *            id of the mms message which holds the attachments
	 * @return attachment as byte array
	 * @throws com.klikmee.sms.core.ws.mts.client.receivemsg.ServiceException_Exception
	 * @throws com.klikmee.sms.core.ws.mts.client.receivemsg.PolicyException_Exception
	 * @throws IOException
	 */
	public byte[] getMMSAttachments(String messageIdentifier) throws com.klikmee.sms.core.ws.mts.client.receivemsg.PolicyException_Exception,
			com.klikmee.sms.core.ws.mts.client.receivemsg.ServiceException_Exception, IOException {
		// call getMessage method on proxy
		receiveMessageProxy.getMessage( messageIdentifier );

		// Although method is void attachments are returned
		Map<String, Object> responseContext = ((BindingProvider) this.receiveMessageProxy).getResponseContext();
		HashMap attatchments = (HashMap) responseContext.get( MessageContext.INBOUND_MESSAGE_ATTACHMENTS );
		// get the datahandler key
		String key = attatchments.keySet().toString();
		// remove first and last char
		key = key.substring( 1, key.length() - 1 );
		DataHandler dataHandler = (DataHandler) attatchments.get( key );

		// read the datahandler input stram
		InputStream inputStream = dataHandler.getInputStream();
		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
		// read until end of stram is reached
		int next = inputStream.read();
		while (next != -1) {
			byteArrayOutputStream.write( next );
			next = inputStream.read();
		}

		byteArrayOutputStream.flush();
		return byteArrayOutputStream.toByteArray();

	}

	public void notifyMessageDeliveryReceipt(String correlator, String address, com.klikmee.sms.core.ws.mts.listener.msgnotification.DeliveryStatus deliveryStatus) {
		// TODO Auto-generated method stub

	}

	@Override
	public List<SendSmsIntercepter> getSendSmsIntercepters() {

		return this.sendSmsIntercepters;
	}

	private int resolveDeliveryStatus(DeliveryStatus mtsStatus) {
		switch (mtsStatus) {
		case DELIVERED_TO_NETWORK:
		case DELIVERED_TO_TERMINAL:
			return DeliveryReports.DELIVERED;

		case DELIVERY_IMPOSSIBLE:
			return DeliveryReports.Mts.DELIVERY_IMPOSSIBLE;
		case DELIVERY_NOTIFICATION_NOT_SUPPORTED:
			return DeliveryReports.Mts.DELIVERY_NOTIFICATION_NOT_SUPPORTED;

		case DELIVERY_UNCERTAIN:
			return DeliveryReports.Mts.DELIVERY_UNCERTAIN;
		case MESSAGE_WAITING:
			return DeliveryReports.Mts.DELIVERY_UNCERTAIN;
		default:
			return -1;
		}
	}

	public void setSendSMSURL(URL sendSMSURL) {
		this.sendSMSURL = sendSMSURL;
	}

	public void setReceiveMessageURL(URL receiveMessageURL) {
		this.receiveMessageURL = receiveMessageURL;
	}

	public void setSubscriptionManager(SubscriptionManager subscriptionManager) {
		this.subscriptionManager = subscriptionManager;
	}

	public void setCore(Core core) {
		this.core = core;
	}

	public void setProviderDAO(ProviderDAO providerDAO) {
		this.providerDAO = providerDAO;
	}

	public void setSendSmsOverrideAddress(String sendSmsOverrideAddress) {
		this.sendSmsOverrideAddress = sendSmsOverrideAddress;
	}

	public String getProviderId() {
		return providerId;
	}

	public void setProviderId(String providerId) {
		this.providerId = providerId;
	}

	public URL getSendSMSURL() {
		return sendSMSURL;
	}

	public String getDeliveryReportNotificationURL() {
		return deliveryReportNotificationURL;
	}

	public void setDeliveryReportNotificationURL(String deliveryReportNotificationURL) {
		this.deliveryReportNotificationURL = deliveryReportNotificationURL;
	}

	public String getSendSmsOverrideAddress() {
		return sendSmsOverrideAddress;
	}

	public void setSendSmsIntercepters(List<SendSmsIntercepter> sendSmsIntercepters) {
		this.sendSmsIntercepters = sendSmsIntercepters;
	}

	public void setProviderPassword(String providerPassword) {
		this.providerPassword = providerPassword;
	}

	public String getProviderPassword() {
		return providerPassword;
	}

	/**
	 * 
	 * Inner class used to provide callback for MTS web service auth.
	 * 
	 */

	public static class ClientPasswordCallback implements CallbackHandler {

		private static Logger		LOGGER			= Logger.getLogger( ClientPasswordCallback.class );

		private static String		PASSWORD		= null;
		private static Properties	CORE_SETTINGS	= new Properties();

		static {

			InputStream propertiesStream = MTSCommunicatorImpl.class.getClassLoader().getResourceAsStream( "sms-core.properties" );
			try {
				CORE_SETTINGS.load( propertiesStream );
				// read password from properties file
				PASSWORD = CORE_SETTINGS.getProperty( "MTS_PROVIDER_PASSWORD" );
			}
			catch (IOException e) {
				LOGGER.fatal( "Could not read sms-core.properties" );
			}
		}

		public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {

			// if password is null, break sms sending

			if (PASSWORD == null) {
				throw new RuntimeException( "MTS password not set ... " );
			}
			WSPasswordCallback callback = (WSPasswordCallback) callbacks[0];
			callback.setPassword( PASSWORD );

		}

	}

	public class ProviderPasswordCallback implements CallbackHandler {

		@Override
		public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {

			if (MTSCommunicatorImpl.this.getProviderPassword() == null) {
				throw new RuntimeException( "MTS password not set ... " );
			}

			WSPasswordCallback callback = (WSPasswordCallback) callbacks[0];
			callback.setPassword( MTSCommunicatorImpl.this.getProviderPassword() );
		}
	}

	@Override
	public Provider getProvider() {
		// TODO Auto-generated method stub
		return null;
	}

}
